/******************************************************************************
	Cobalt - utility libraries for building XNA games
	Copyright (C) 2008 Philip Djonov

	This program is free software; you can redistribute it and/or modify it
	under the terms of the GNU General Public License as published by the Free
	Software Foundation; either version 2 of the License, or (at your option)
	any later version.

	This program is distributed in the hope that it will be useful, but
	WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
	or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
	for more details.

	You should have received a copy of the GNU General Public License along
	with this program; if not, write to the
	
		Free Software Foundation, Inc.
		51 Franklin Street, Fifth Floor
		Boston, MA  02110-1301, USA.
******************************************************************************/

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;

using GdiFont = System.Drawing.Font;
using GdiGraphics = System.Drawing.Graphics;
using GdiRectangle = System.Drawing.Rectangle;

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Content.Pipeline;
using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
using Microsoft.Xna.Framework.Graphics.PackedVector;

using XnaColor = Microsoft.Xna.Framework.Graphics.Color;
using XnaRectangle = Microsoft.Xna.Framework.Rectangle;

namespace Cobalt.Content.Pipeline.Graphics.Font
{
	[ContentProcessor( DisplayName = "Cobalt Font Processor" )]
	public class SpriteFontProcessor : ContentProcessor<SpriteFontDescription, SpriteFontContent>
	{
		#region Helper Types
		protected class FontProcessHelper : IDisposable
		{
			#region Setup and Teardown

			private IntPtr dc, hFont, hOldFont;
			private GdiFont font;
			private Bitmap drawBmp;
			private GdiGraphics drawGraphics;

			/// <summary>
			/// A GDI HDC that has <see cref="HFont"/> selected into it.
			/// </summary>
			protected IntPtr HDC { get { return dc; } }
			/// <summary>
			/// A GDI HFONT describing the font we're currently rendering.
			/// </summary>
			protected IntPtr HFont { get { return hFont; } }
			/// <summary>
			/// A <see cref="System.Drawing.Bitmap"/> used to render the glyph image.
			/// </summary>
			/// <remarks>
			/// Note that the value of this may change across calls to <see cref="SetupTempImage"/>
			/// </remarks>
			protected Bitmap TempImage { get { return drawBmp; } }
			/// <summary>
			/// A <see cref="System.Drawing.Graphics"/> used to render the glyph image.
			/// </summary>
			/// <remarks>
			/// Note that the value of this may change across calls to <see cref="SetupTempImage"/>
			/// </remarks>
			protected GdiGraphics Graphics { get { return drawGraphics; } }

			public FontProcessHelper( string fontName, float size, FontStyle style )
			{
				float emSize = size * 96F / 72F;
				font = new GdiFont( fontName, emSize, style, GraphicsUnit.Pixel );

				SetupTempImage( 64, 64 );
				
				dc = CreateCompatibleDC( IntPtr.Zero );
				if( dc == IntPtr.Zero )
					throw new System.ComponentModel.Win32Exception();

				hFont = font.ToHfont();
				hOldFont = SelectObject( dc, hFont );
			}

			/// <summary>
			/// Ensures that <see cref="TempImage"/> is at least <paramref name="width"/>
			/// by <paramref name="height"/> pixels and clears it to opaque black.
			/// </summary>
			/// <param name="width">The minimum width of the temp image.</param>
			/// <param name="height">The minimum height of the temp image.</param>
			/// <remarks>
			/// Note that the temp image may be larger on return. If the temp image
			/// is resized both <see cref="TempImage"/> and <see cref="Graphics"/> will
			/// change their values (though they will be set up the same way).
			/// </remarks>
			protected void SetupTempImage( int width, int height )
			{
				if( drawBmp != null )
				{
					if( drawBmp.Width >= width || drawBmp.Height >= height )
					{
						drawGraphics.Clear( Color.Black );
						return;
					}

					width = Math.Max( width, drawBmp.Width );
					height = Math.Max( height, drawBmp.Height );
				}

				Helpers.DisposeAndNull( ref drawGraphics );
				Helpers.DisposeAndNull( ref drawBmp );

				drawBmp = new Bitmap( width, height );
				drawGraphics = GdiGraphics.FromImage( drawBmp );

				drawGraphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
				drawGraphics.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.HighQuality;
				drawGraphics.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;

				drawGraphics.Clear( Color.Black );
			}

			protected virtual void Dispose( bool disposing )
			{
				if( dc != IntPtr.Zero )
				{
					SelectObject( dc, hOldFont );
					DeleteObject( hFont );
					DeleteDC( dc );

					dc = IntPtr.Zero;
				}

				if( disposing )
				{
					Helpers.DisposeAndNull( ref drawGraphics );
					Helpers.DisposeAndNull( ref drawBmp );
					Helpers.DisposeAndNull( ref font );
				}
			}

			public void Dispose()
			{
				Dispose( true );
				GC.SuppressFinalize( this );
			}

			~FontProcessHelper()
			{
				Dispose( false );
			}
			#endregion

			#region Kerning Stuff
			ushort[] idxs = new ushort[1];
			private bool FontContainsChar( int ch )
			{
				if( GetGlyphIndicesW( dc, new string( (char)ch, 1 ), 1, idxs, GetGlyphIndicesFlags.MarkNonexistingGlyphs ) == uint.MaxValue )
					throw new System.ComponentModel.Win32Exception();

				return idxs[0] != 0xFFFF;
			}

			private ABCFloat[] abcs = new ABCFloat[1];
			public virtual void GetKerningInfo( GlyphContent glyph )
			{
				if( !FontContainsChar( glyph.Character ) )
				{
					glyph.AbcKernA = 0;
					glyph.AbcKernB = glyph.Size.Width;
					glyph.AbcKernC = 0;

					return;
				}

				GetCharABCWidthsFloatW( dc, (uint)glyph.Character, (uint)glyph.Character, abcs );

				glyph.AbcKernA = abcs[0].A;
				glyph.AbcKernB = abcs[0].B;
				glyph.AbcKernC = abcs[0].C;
			}
			#endregion
			
			#region Bitmap Rendering
			private unsafe byte GetPixelValue( BitmapData data, int x, int y )
			{
				if( x < 0 || x >= data.Width || y < 0 || y >= data.Height )
					throw new ArgumentOutOfRangeException();

				byte* p = (byte*)data.Scan0 + y * data.Stride + x * 4;

				//is this guaranteed to be grayscale?
				//can I just use the r channel and be done with it?!

				float r = (float)p[2] / 0xFF;
				float g = (float)p[1] / 0xFF;
				float b = (float)p[0] / 0xFF;

				return Helpers.PackFloat( r * 0.3F + g * 0.59F + b * 0.11F );
			}

			private bool ColumnIsEmpty( BitmapData data, int c )
			{
				if( c < 0 || c >= data.Width )
					return false;

				for( int i = 0; i < data.Height; i++ )
					if( GetPixelValue( data, c, i ) != 0 )
						return false;

				return true;
			}

			private bool RowIsEmpty( BitmapData data, int r )
			{
				if( r < 0 || r >= data.Height )
					return false;

				for( int i = 0; i < data.Width; i++ )
					if( GetPixelValue( data, i, r ) != 0 )
						return false;

				return true;
			}

			private byte[][] GetImageAlphaBytes( ref PointF glyphOffset )
			{
				BitmapData bmpDat = null;
				try
				{
					GdiRectangle rc = new GdiRectangle( 0, 0, drawBmp.Width, drawBmp.Height );
					bmpDat = drawBmp.LockBits( rc, ImageLockMode.ReadOnly, PixelFormat.Format32bppRgb );

					//find the portion that actually contains data

					while( ColumnIsEmpty( bmpDat, rc.Right - 1 ) )
						rc.Width--;

					while( ColumnIsEmpty( bmpDat, rc.Left ) )
					{
						rc.X++;
						rc.Width--;
					}

					while( RowIsEmpty( bmpDat, rc.Bottom - 1 ) )
						rc.Height--;

					while( RowIsEmpty( bmpDat, rc.Top ) )
					{
						rc.Y++;
						rc.Height--;
					}

					if( rc.Right <= rc.Left || rc.Bottom <= rc.Top )
						return GlyphContent.EmptyImage;

					glyphOffset = new PointF( rc.Location.X - glyphOffset.X, rc.Location.Y - glyphOffset.Y );

					//copy it out into a managed array

					byte[][] ret = new byte[rc.Height][];
					for( int y = 0; y < rc.Height; y++ )
					{
						ret[y] = new byte[rc.Width];
						for( int x = 0; x < rc.Width; x++ )
							ret[y][x] = GetPixelValue( bmpDat, rc.Left + x, rc.Top + y );
					}
					return ret;
				}
				finally
				{
					if( bmpDat != null )
						drawBmp.UnlockBits( bmpDat );
				}
			}

			/// <summary>
			/// Draws the glyph onto the temp bitmap.
			/// </summary>
			/// <param name="glyph">The glyph to render.</param>
			/// <param name="glyphOffset">The offset in <see cref="TempImage"/> the glyph was rendered at.</param>
			/// <param name="glyphSize">The size of the rendered glyph.</param>
			protected virtual void DrawImage( GlyphContent glyph, out PointF glyphOffset, out Size glyphSize )
			{
				string s = new string( (char)glyph.Character, 1 );

				glyphSize = Size.Ceiling( drawGraphics.MeasureString( s, font ) );

				SetupTempImage(
					glyphSize.Width + 32,
					glyphSize.Height + 32 );

				glyphOffset = new PointF( 8, 8 );

				drawGraphics.DrawString( s, font, Brushes.White, glyphOffset );
				drawGraphics.Flush();
			}

			public void Rasterize( GlyphContent glyph )
			{
				Size glyphSize;
				PointF glyphOffset;
				DrawImage( glyph, out glyphOffset, out glyphSize );

				byte[][] glyphData = GetImageAlphaBytes( ref glyphOffset );
				glyphOffset.X = 0;

				float yOfs = (font.FontFamily.GetEmHeight( font.Style ) - (font.FontFamily.GetCellAscent( font.Style ) + font.FontFamily.GetCellDescent( font.Style ))) *
					(font.GetHeight() / font.FontFamily.GetEmHeight( font.Style ));
				glyphOffset.Y -= yOfs;

				glyph.Size = glyphSize;

				glyph.ImageData = glyphData;
				glyph.DrawOffset = glyphOffset;
			}
			#endregion

			/// <summary>
			/// Sets the <see cref="FontPointContent.LineSpacing"/> and
			/// <see cref="FontPointContent.Ascent"/> members of <paramref name="point"/>.
			/// </summary>
			/// <param name="point">The <see cref="FontPointContent"/> to initialize.</param>
			public virtual void GetPointInfo( FontPointContent point )
			{
				point.LineSpacing = font.GetHeight();

				float emToUnits = point.LineSpacing / font.FontFamily.GetEmHeight( font.Style );
				point.Ascent = font.FontFamily.GetCellAscent( font.Style ) * emToUnits;
			}

			#region Interop Definitions
			private const string GdiDll = "Gdi32.dll";

			[DllImport( GdiDll, SetLastError = true )]
			private static extern IntPtr CreateCompatibleDC( IntPtr dc );

			[DllImport( GdiDll, SetLastError = true )]
			private static extern IntPtr SelectObject( IntPtr dc, IntPtr hObj );

			[DllImport( GdiDll, SetLastError = true )]
			private static extern int DeleteObject( IntPtr obj );

			[DllImport( GdiDll, SetLastError = true )]
			private static extern int DeleteDC( IntPtr dc );

			[StructLayout( LayoutKind.Sequential )]
			private struct ABCFloat
			{
				public float A, B, C;
			}

			[DllImport( GdiDll, SetLastError = true )]
			private static extern int GetCharABCWidthsFloatW( IntPtr dc, uint iFirstChar, uint iLastChar, [Out] ABCFloat[] abcFloat );

			[Flags]
			private enum GetGlyphIndicesFlags : uint
			{
				MarkNonexistingGlyphs = 0x00000001,
			}

			[DllImport( GdiDll, SetLastError = true )]
			private static extern uint GetGlyphIndicesW( IntPtr dc, [In, MarshalAs( UnmanagedType.LPWStr )] string cch, int numChars, [Out] ushort[] indices, GetGlyphIndicesFlags flags );
			#endregion
		}

		private sealed class GlyphPacker
		{
			private Size maxSheetSize, padding;
			public GlyphPacker( Size maxSheetSize, Size padding )
			{
				this.maxSheetSize = maxSheetSize;
				this.padding = padding;
			}

			private List<Texture2DContent> sheets = new List<Texture2DContent>();

			private int x, y, curLineHeight;
			private PixelBitmapContent<XnaColor> sheet;

			private List<GlyphContent> currentSheetGlyphs = new List<GlyphContent>();

			private void InsertGlyph( GlyphContent glyph )
			{
				Size sz = glyph.ImageDataSize;

				for( int sy = 0; sy < sz.Height; sy++ )
				{
					for( int sx = 0; sx < sz.Width; sx++ )
					{
						sheet.SetPixel( x + sx, y + sy, new XnaColor( 0xFF, 0xFF, 0xFF, glyph.ImageData[sy][sx] ) );
					}
				}

				currentSheetGlyphs.Add( glyph );
				glyph.ImageSheetRectangle = new GdiRectangle( x, y, sz.Width, sz.Height );
			}

			private void FinishSheet()
			{
				int height = sheet.Height;
				while( y + curLineHeight < height / 2 )
					height /= 2;

				if( height < sheet.Height )
				{
					PixelBitmapContent<XnaColor> newSheet = new PixelBitmapContent<XnaColor>( sheet.Width, height );
					BitmapContent.Copy( sheet, new XnaRectangle( 0, 0, sheet.Width, height ),
						newSheet, new XnaRectangle( 0, 0, sheet.Width, height ) );
					sheet = newSheet;
				}

				Texture2DContent sheetImg = new Texture2DContent();
				sheetImg.Mipmaps.Add( sheet );

				foreach( GlyphContent glyph in currentSheetGlyphs )
					glyph.ImageSheet = sheetImg;

				sheets.Add( sheetImg );
				
				sheet = null;
				x = y = curLineHeight = 0;

				currentSheetGlyphs.Clear();
			}

			public void PackGlyphs( IEnumerable<GlyphContent> glyphs )
			{
				//ToDo: sort the glyphs???

				foreach( GlyphContent glyph in glyphs )
				{
					Size sz = glyph.ImageDataSize + padding;

					if( sz.Width > maxSheetSize.Width || sz.Height > maxSheetSize.Height )
						throw new InvalidContentException( "Glyph is bigger than sprite sheet." );

					if( sheet != null )
					{
						if( x + sz.Width > sheet.Width )
						{
							y += curLineHeight;
							curLineHeight = 0;
							x = padding.Width;

							if( y + sz.Height > sheet.Height )
								FinishSheet();
						}
						else if( y + sz.Height > sheet.Height )
							FinishSheet();
					}

					if( sheet == null )
					{
						sheet = new PixelBitmapContent<XnaColor>( maxSheetSize.Width, maxSheetSize.Height );
						x = padding.Width;
						y = padding.Height;
						curLineHeight = 0;
					}
					
					//it fits!
					InsertGlyph( glyph );					

					//advance the current position
					curLineHeight = Math.Max( curLineHeight, sz.Height );
					x += sz.Width;					
				}
			}

			public Texture2DContent[] BuildSheets()
			{
				if( sheet != null )
					FinishSheet();

				return sheets.ToArray();
			}
		}
		#endregion

		protected virtual FontProcessHelper CreateHelper( SpriteFontDescription input, float size,
			ContentProcessorContext context )
		{
			return new FontProcessHelper( input.FontName, size, input.Style );
		}

		/// <summary>
		/// Allows a derived class to adjust the input before glyphs are rendered.
		/// </summary>
		/// <param name="input">The <see cref="SpriteFontDescription"/> to adjust.</param>
		/// <remarks>
		/// By default this method simply ensures the input contains the default character set.
		/// </remarks>
		protected virtual void AdjustInput( SpriteFontDescription input, ContentProcessorContext context )
		{
			input.Characters.AddDefaultCharacters();
		}

		/// <summary>
		/// Allows a derived class to do any custom processing after a point is rendered
		/// but before the glyphs are laid out in texture sheets.
		/// </summary>
		/// <param name="point">The fully rendered and measured <see cref="FontPointContent"/>.</param>
		protected virtual void PostProcessFontPoint( FontPointContent point, ContentProcessorContext context )
		{
		}

		public override SpriteFontContent Process( SpriteFontDescription input, ContentProcessorContext context )
		{
			AdjustInput( input, context );

			int[] characters = input.Characters.GetCharList();
			if( characters.Length == 0 )
				throw new InvalidContentException( "Font does not contain any characters.", input.Identity );

			float[] sizes = input.Sizes.GetSizeList();
			if( sizes.Length == 0 )
				throw new InvalidContentException( "Font does not contain any point sizes.", input.Identity );

			FontPointContent[] points;
			Texture2DContent[] sheets;

			try
			{
				points = new FontPointContent[sizes.Length];
				for( int iPt = 0; iPt < points.Length; iPt++ )
				{
					FontPointContent point = new FontPointContent();

					point.Size = sizes[iPt];
					point.Glyphs = new GlyphContent[characters.Length];

					using( FontProcessHelper helper = CreateHelper( input, point.Size, context ) )
					{
						helper.GetPointInfo( point );

						for( int iCh = 0; iCh < characters.Length; iCh++ )
						{
							GlyphContent glyph = new GlyphContent( characters[iCh] );

							helper.Rasterize( glyph );
							helper.GetKerningInfo( glyph );

							point.Glyphs[iCh] = glyph;
						}
					}

					PostProcessFontPoint( point, context );

					points[iPt] = point;
				}

				GlyphPacker packer = new GlyphPacker( input.MaxImageSize, new Size( 2, 2 ) );
				foreach( FontPointContent point in points )
					packer.PackGlyphs( point.Glyphs );
				sheets = packer.BuildSheets();
			}
			catch( Exception ex )
			{
				throw new InvalidContentException( "Error building font.", input.Identity, ex );
			}
			
			SpriteFontContent ret = new SpriteFontContent();
			
			ret.Name = input.Name;
			ret.Identity = input.Identity;

			ret.Characters = characters;
			ret.FontPoints = points;
			ret.ImageSheets = sheets;

			return ret;
		}
	}
}